package down.sallai.ml.util;


import lombok.extern.slf4j.Slf4j;

import javax.net.ssl.*;
import javax.servlet.http.HttpServletRequest;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.RandomAccessFile;
import java.net.HttpURLConnection;
import java.net.URL;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ThreadPoolExecutor;

@Slf4j
public class HttpsDownUtils {


    private final static int THREAD_NUMBER = 3;
    private String mFileUrl;
    private final static ThreadPoolExecutor mThreadPoolExecutor = MyThreadPool.getThreadPoolExecutor(); //获取线程池
    private volatile Boolean cancelled = false;
    private long mStartTime = System.currentTimeMillis();
    private long mTaskTimeout = 60 * 3 * 1000;

    /**
     * @param fileUrl   https 远程路径
     * @param fileLocal 本地文件存放路径,需要注意的是这里是要传一个已经存在的文件，否则会报拒绝访问的的错误
     * @throws Exception
     */
    public String downloadFile(String fileUrl, String fileLocal, HttpServletRequest request) throws Exception {
        boolean isRange = false;
        String fileName = System.currentTimeMillis() + "_" + fileUrl.substring(fileUrl.lastIndexOf("/") + 1);
        fileLocal = fileLocal + fileName;
        mFileUrl = fileUrl;
        HashMap<String, String> property = new HashMap<>();
        property.put("Range", "bytes=0-");
        HttpURLConnection urlCon = getHttpURLConnection(fileUrl, property);
        if( null == urlCon) throw new NullPointerException("获取连接长度的urlCon为null");
        int code = urlCon.getResponseCode();
        if (code != HttpURLConnection.HTTP_OK && code != 206) {
            log.error("http 响应状态异常 code:" + code);
            throw new Exception("连接文件失败！");
        }
        int contentLength = urlCon.getContentLength();
        InputStream inputStream = urlCon.getInputStream();
        String isRanges = urlCon.getHeaderField("Accept-Ranges");
        log.info("Accept-Ranges:"+isRanges);
        if(null != isRanges && isRanges.trim().equals("bytes")) {
            isRange = true;
        }else{
            isRange = false;
        }
        if (!isRange || contentLength == -1) {
            oneThreadDownload(request, inputStream, fileLocal);
        } else {
            multiThreadDownload(contentLength, fileLocal, request,fileUrl);
        }
        //关闭流
        return fileName;
    }

    /**
     * one thread download the file
     *
     * @param request
     * @param inputStream
     * @param fileLocal
     * @throws IOException
     */

    private void oneThreadDownload(HttpServletRequest request, InputStream inputStream, String fileLocal) throws Exception{

        FileOutputStream fileOut = null;
        try {
            request.getSession().setAttribute("downProcess", null);
            log.info("单线程下载模式");
            fileOut = new FileOutputStream(fileLocal);
            int sum = 0;
            byte[] b = new byte[1024 * 3];
            int read = inputStream.read(b);


            while (read > 0) {
                sum += read;
                String r = "";
                if (sum > 0 && sum < 1024) {

                    r = sum + "byte";
                } else if (sum >= (1024) && sum < (1024 * 1024)) {
                    r = sum / (1024) + "kb";
                } else if (sum >= (1024 * 1024) && sum < (1024 * 1024 * 1024)) {
                    r = sum / (1024 * 1024) + "mb";
                } else {

                }
                request.getSession().setAttribute("downProcess", r);
                log.info("单线程下载进度："+r);
                fileOut.write(b, 0, read);
                read = inputStream.read(b);

            }
        }catch (Exception e){
            if( null != fileOut ) {
                try {
                    fileOut.close();
                } catch (IOException ioException) {
                    ioException.printStackTrace();
                }
                fileOut = null;
            }

            if ( null != inputStream) {
                try {
                    inputStream.close();
                } catch (IOException ioException) {
                    ioException.printStackTrace();
                }
                inputStream = null;
            }
            throw new Exception("单线程下载异常");
         }
    }


    public HttpURLConnection getHttpURLConnection(String fileUrl, HashMap<String, String> requestProperty) {
        HttpsURLConnection urlCon = null;
        try {
            SSLContext sslcontext = SSLContext.getInstance("SSL", "SunJSSE");
            sslcontext.init(null, new TrustManager[]{new X509TrustUtiil()}, new java.security.SecureRandom());
            URL url = new URL(fileUrl);
            HostnameVerifier ignoreHostnameVerifier = new HostnameVerifier() {
                @Override
                public boolean verify(String s, SSLSession sslsession) {
                    log.warn("Hostname is not matched for cert.");
                    return true;
                }
            };
            HttpsURLConnection.setDefaultHostnameVerifier(ignoreHostnameVerifier);
            HttpsURLConnection.setDefaultSSLSocketFactory(sslcontext.getSocketFactory());
            urlCon = (HttpsURLConnection) url.openConnection();
            urlCon.setConnectTimeout(20000);
            urlCon.setReadTimeout(20000);
            //通用的参数
            urlCon.setRequestProperty("Accept-Encoding", "identity");
            //设置请求参数
            if (null != requestProperty) {
                Set<String> strings = requestProperty.keySet();
                for (String key : strings) {
                    urlCon.setRequestProperty(key, requestProperty.get(key));
                }
            }
            return urlCon;
        } catch (Exception e) {
          if( null != urlCon ) {
              urlCon.disconnect();
              urlCon = null;
          }
        }finally {
            return urlCon;
        }

    }

    /**
     * @param contentLength
     * @param fileLocal
     * @param request
     * @throws Exception
     */
    private void multiThreadDownload(long contentLength, String fileLocal, HttpServletRequest request,String url) throws Exception {
        log.info("准备多线程下载任务，线程数->" + THREAD_NUMBER + " 任务总大小->" + contentLength);
        long point = contentLength / THREAD_NUMBER + 1;
        RandomAccessFile file = new RandomAccessFile(fileLocal, "rw");
        //设置本地文件的大小
        file.setLength(contentLength);
        file.close();
        CountDownLatch countDownLatch = new CountDownLatch(THREAD_NUMBER);
        ArrayList<MyRunable> myRunables = new ArrayList<>();
        for (int i = 0; i < THREAD_NUMBER; i++) {
            RandomAccessFile f = new RandomAccessFile(fileLocal, "rw");
            long start = i * point;
            f.seek(start);
            long end = start + point;
            long curPoint = end > contentLength ? contentLength - start : point;
            MyRunable myRunable = new MyRunable(start, curPoint, f, countDownLatch,url);
            myRunables.add(myRunable);
            mThreadPoolExecutor.execute(myRunable);
        }
        long sum = 0;
        while (countDownLatch.getCount()!=0) {
            //判断超时取消任务
            if( (System.currentTimeMillis() - mStartTime) > mTaskTimeout ) {
                log.info("超时取消任务");
                cancelled = true;
            }
            sum = 0;
            for (MyRunable t : myRunables) {
                sum += t.length;
            }
            Thread.sleep(1000);
            long process = (sum * 100) / contentLength;
            log.info("当前任务下载进度->" + process);
            request.getSession().setAttribute("downProcess", String.valueOf(process));
        }
        countDownLatch.await();
        if(cancelled) {
            throw new Exception();
        }
        log.info("多线程下载任务完成");
    }

    /**
     * 用于ssl证书
     */
    static class X509TrustUtiil implements X509TrustManager {

        @Override
        public void checkClientTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException {

        }

        @Override
        public void checkServerTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException {

        }

        @Override
        public X509Certificate[] getAcceptedIssuers() {
            return null;
        }

    }


    /**
     * implement the part download logic
     */
    class MyRunable implements Runnable {
        //当前线程的下载位置
        private long startPos;
        //定义当前线程负责下载的文件大小
        private long currentPartSize;
        //当前线程需要下载的文件块
        private RandomAccessFile currentPart;
        //定义该线程已下载的字节数
        public long length = 0;
        private CountDownLatch countDownLatch;
        private String mUrl;
        private int mReconnectCount = 0;

        public MyRunable(long start, long currentPartSize, RandomAccessFile currentPart, CountDownLatch countDownLatch,String url){
            this.startPos = start;
            this.currentPartSize = currentPartSize;
            this.currentPart = currentPart;
            this.countDownLatch = countDownLatch;
            this.mUrl = url;
        }

        @Override
        public void run() throws NullPointerException{
            System.out.println(Thread.currentThread().getName() + "执行任务,起点->" + startPos + "负责大小->" + currentPartSize);
            SSLContext sslcontext = null;
            InputStream in = null;
            HttpURLConnection urlCon = null;
            try {
                HashMap<String, String> property = new HashMap<>();

                property.put("Range","bytes=" + startPos + "-" + (startPos + currentPartSize));
                log.info(property.get("Range"));
                urlCon = getHttpURLConnection(mUrl, property);
                if( null == urlCon ) {
                    while(mReconnectCount < 2 && null != urlCon) {
                        urlCon = getHttpURLConnection(mUrl, property);
                        mReconnectCount++;
                    }
                    if( null == urlCon ) throw new Exception("多线程任务连接失败");
                }
                int code = urlCon.getResponseCode();
                System.out.println(code);
                if (code != 206) {
                    throw new Exception("服务端响应不是分片下载");
                }
                log.info("连接完成");
                // 读文件流
                
                in = urlCon.getInputStream();
                byte[] buffer = new byte[1024 * 3];
                int count = 0;
                while (!cancelled && length < currentPartSize && (count = in.read(buffer)) > 0) {
                    currentPart.write(buffer, 0, count);
                    this.length += count;
                }
                log.info((cancelled==true ? "该任务取消" : "流读取完毕") + Thread.currentThread().getId());
            } catch (Exception e) {
                e.printStackTrace();
                cancelled = true;
                throw new NullPointerException("单线程下载异常，终止当前任务");

            } finally {
                countDownLatch.countDown();
                log.info(Thread.currentThread().getName() + "执行完毕！");
                try {
                    if ( null != urlCon ) {
                        urlCon.disconnect();
                        urlCon = null;
                    }
                    currentPart.close();
                    if( null != in ) {
                        in.close();
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                }

            }


        }
    }


}


